0%

ARM pwn 环境搭建+基础入门

ARM 架构简述

ARM 架构,过去称作进阶精简指令集机器(Advanced RISC Machine,更早称作艾康精简指令集机器, Acorn RISC Machine),是一个精简指令集(RISC)处理器架构家族,其广泛地使用在许多嵌入式系统设计

ARM程序,指在ARM系统中正在执行的程序,而非保存在ROM中的bin文件

ARM 和 x86 之间的更多区别是:

  • 在 ARM 中,大多数指令都可用于条件执行。
  • 英特尔 x86 和 x86-64 系列处理器使用小端格式
  • ARM 架构在低版本是小端的,之后,ARM 处理器成为 BL端(有允许可切换字节序的设置,由程序状态寄存器 CPSR 的位9 (E位) 控制)

一个ARM程序包含3部分:

  • RO段(只读):RO是程序中的指令和常量
  • RW段(可读写):RW是程序中已初始化的变量
  • ZI段(可读写):ZI是程序中未初始化的变量

搭建环境

安装 qemu-user:(用于开启虚拟机)

1
2
sudo apt-get install qemu-user
sudo apt-get install qemu-use-binfmt qemu-user-binfmt:i386

安装交叉编译工具 aarch64-linux-gnu-gcc:(推荐)

1
sudo apt-get install gcc-aarch64-linux-gnu

安装交叉编译工具链 arm-linux-gcc-4.3.3:(不推荐)

1
2
3
sudo mkdir /usr/local/arm_4.4.3
sudo cp -r ./opt/FriendlyARM/toolschain/4.4.3/* /usr/local/arm_4.4.3
gedit ~/.zshrc
1
export PATH=$PATH:/usr/local/arm_4.4.3/bin

安装 qemu 内核:(最好不要 apt-get 直接安装,因为没有 qemu-system-aarch64

1
2
3
4
5
6
wget https://download.qemu.org/qemu-6.2.0.tar.xz
tar xvJf qemu-6.2.0.tar.xz
cd qemu-6.2.0
./configure --target-list=aarch64-softmmu
make
sudo make install

获取 ubuntu-18.04-server-arm64.iso:

下载对应架构(aarch64)的 UEFI 固件:

1
wget http://releases.linaro.org/components/kernel/uefi-linaro/16.02/release/qemu64/QEMU_EFI.fd

创建虚拟机硬盘:

1
qemu-img create ubuntuimg.img 40G

创建虚拟机:

1
qemu-system-aarch64 -m 2048 -cpu cortex-a57 -smp 2 -M virt -bios QEMU_EFI.fd -nographic -drive if=none,file=ubuntu-18.04-server-arm64.iso,id=cdrom,media=cdrom -device virtio-scsi-device -device scsi-cd,drive=cdrom -drive if=none,file=ubuntuimg.img,id=hd0 -device virtio-blk-device,drive=hd0

直接回车选择安装 Ubuntu Server:

1661655283051

安装成功后,就可以通过如下命令开启 ARM 程序:

1
2
qemu-arm -L /usr/aarch64-linux-gnu -g 1234 ./[pwn] 
qemu-aarch64 -L /usr/aarch64-linux-gnu -g 1234 ./[pwn]

最后介绍一下调试方法:

1
2
3
gdb-multiarch [pwn] 
set architecture [Arch-name] # aarch64 or arm
target remote localhost:1234

参考:

数据类型和寄存器

与高级语言类似,ARM 支持对不同数据类型的操作

我们可以加载(或存储)的数据类型可以是有符号和无符号单词,半字或字节,这些数据类型的扩展名是:

  • -h 或 -sh 表示半字
  • -b 或 -sb 表示一字节
  • 没有扩展名表示一字

有符号数据类型和无符号数据类型之间的区别在于:

  • 有符号数据类型 (+s) 可以同时包含正值和负值,因此范围较小
  • 无符号数据类型 (+0) 可以保存较大的正值(包括“零”),但不能保存负值,因此范围更广

以下是如何将这些数据类型与加载和存储说明一起使用的一些示例:

1
2
3
4
5
6
7
8
9
10
11
ldr = Load Word
ldrh = Load unsigned Half Word
ldrsh = Load signed Half Word
ldrb = Load unsigned Byte
ldrsb = Load signed Bytes

str = Store Word
strh = Store unsigned Half Word
strsh = Store signed Half Word
strb = Store unsigned Byte
strsb = Store signed Byte

寄存器的数量取决于ARM版本

根据ARM参考手册,除了基于 ARMv6-MARMv7-M 的处理器外,都有30个通用的32位寄存器,其中的 r0-r15 作用如下表:

32位 64位 别名 目的
R0-R6 X0-X7 一般用途
X8 保存子程序返回值
R7 持有系统调用号
X9-X15 临时寄存器 子程序使用时不需要保存
R8-R10 X19-X28 临时寄存器 子程序使用时必须保存
X18 记录平台信息
R11 X29 FP 帧指针
R12 X16-X17 IP 程序内呼叫
R13 X31 SP 栈指针
R14 X30 LR 链接注册
R15 PC 程序计数器
CPSR CPSR 当前程序状态寄存器
SPSR SPSR 程序状态保存寄存器
  • R0-R12:可在常见操作期间用于存储临时值、指针(到内存的位置)等
    • R0 在算术运算期间可以称为累加器,或者用于存储以前调用的函数的结果
    • R7 在使用系统调用时变得很有用,因为它存储了 syscall 编号
    • R11 帮助我们跟踪栈上的边界,作为帧指针(稍后将介绍)
    • R0-R3:ARM 上的函数调用约定指定函数的前四个参数存储在寄存器 r0-r3 中
  • R13:SP(栈指针)栈指针指向栈的顶部
    • 栈是用于特定于函数的存储的内存区域,当函数返回时,将回收该内存区域
    • 因此,栈指针用于分配栈上的空间,方法是从栈指针中减去我们要分配的值(以字节为单位),换句话说,如果我们想分配一个32位值,我们从栈指针中减去“4”
  • R14:LR(链路寄存器)进行函数调用时,链接寄存器将使用内存地址进行更新,该内存地址引用从中启动函数的下一条指令
    • 这样做允许程序返回到在“子”函数完成后启动“子”函数调用的“父”函数
  • R15:PC(程序计数器)程序计数器按执行的指令的大小自动递增
    • 此大小在 ARM 状态下始终为4个字节,在 THUMB 模式下始终为2个字节
    • 当执行分支指令时,PC保存目标地址,在执行过程中,PC 将当前指令加 8(两条 ARM 指令)的地址存储在 ARM 状态,将当前指令加 4(两条拇指指令)的地址存储在 Thumb(v1) 状态
    • PS:这与x86不同,在x86中,PC 始终指向要执行的下一条指令

CPSR:显示当前程序状态寄存器(CPSR)的值

  • 在此值下,您可以看到标志 >> thumb, fast, interrupt, overflow, carry, zero, and negative
  • 这些标志表示 CPSR 寄存器中的某些位,并根据 CPSR 的值进行设置,并在激活时变为 粗体
  • N、Z、C 和 V 位与 x86 上 EFLAG 寄存器中的 SF、ZF、CF 和 OF 位相同,它们将用于支持程序集级别的条件和循环中的条件执行:
    • N – 当操作结果为负时设置
    • Z – 当操作结果为零时设置
    • C – 判断无符号数是否溢出
    • V – 判断带符号数是否溢出

1661586815885

下表展示了 CPSR 中各个位的具体作用:

Flag Description
N
(Negative)
如果指令结果产生负数,则启用
Z
(Zero)
如果指令的结果产生零值,则启用
C
(Carry)
如果指令的结果产生一个需要完全表示第 33 位的值,则启用该值
V
(Overflow)
如果指令的结果产生的值不能用 32 位 2 的补码表示,则启用此选项
E
(Endian-bit)
ARM可以在小端序或大端中进行切换,对于小字节序,此位设置为“0”,对于大字节序模式,此位设置为“1”
T
(Thumb-bit)
如果您处于 Thumb 状态,则设置此位,并在您处于 ARM 状态时被禁用
M
(Mode-bits)
这些位指定当前权限模式(USR、SVC 等)
J
(Jazelle)
第三个执行状态,允许某些 ARM 处理器在硬件中执行 Java 字节码

ARM & THUMB

ARM 处理器有两种主要状态,它们可以在其中运行(这里不算Jazelle):ARM 和 Thumb

  • Thumb 是 ARM 体系结构中一种 16位的指令集
  • Thumb 指令集可以看作是 ARM 指令压缩形式的子集,它是为减小代码量而提出,具有16bit的代码密度

这些状态与权限级别无关,例如,在 SVC 模式下运行的代码可以是 ARM 或 Thumb,这两种状态之间的主要区别在于指令集,其中 ARM 状态下的指令始终为32位,而 Thumb 状态的指令为16位(但可以是32位)

了解何时以及如何使用 Thumb 对于我们的 ARM 漏洞利用开发目的尤为重要,在编写 ARM 外壳代码时,我们需要删除 NULL 字节,并使用 16 位 Thumb 指令而不是 32 位 ARM 指令来降低拥有它们的机会

如前所述,有不同的 Thumb 版本,不同的命名只是为了将它们彼此区分开来:(处理器本身将始终将其称为 Thumb)

  • Thumb-1(16 位指令):用于 ARMv6 和早期架构
  • Thumb-2(16 位和 32 位指令):通过添加更多指令并允许它们为 16 位或 32 位宽(ARMv6T2、ARMv7)来扩展 Thumb-1
  • ThumbEE:包括一些针对动态生成的代码(在执行前或执行期间在设备上编译的代码)的一些更改和添加

ARM 和 Thumb 之间的区别:

  • 条件执行:
    • ARM 状态下的所有指令都支持条件执行,某些 ARM 处理器版本允许使用 IT 指令在 Thumb 中执行条件(条件执行导致更高的代码密度,因为它减少了要执行的指令数量,并减少了昂贵的分支指令的数量)
  • 32 位 ARM 和 Thumb 指令:
    • 32 位 Thumb 指令具有 .w 后缀
    • 32 位 ARM 指令没有
  • 桶形移位器:
    • 是另一个独特的 ARM 模式功能,它可用于将多个指令缩小为一个

要切换处理器执行的状态,必须满足以下两个条件之一:

  • 我们可以使用分支指令BX(分支和交换)或BLX(分支,链路和交换),并将目标寄存器的最低有效位设置为1
    • 这可以通过将1添加到偏移量(如0x5530 + 1)来实现
    • 您可能会认为这会导致对齐问题,因为指令是 2 字节或 4 字节对齐的
    • 不够都这不是问题,因为处理器将忽略最低有效位
  • 我们知道,如果当前程序状态寄存器中的 T 位已设置,则处于 Thumb 模式

ARM 指令集

汇编语言由指令组成,这些指令是主要的构建块,ARM 指令后面通常跟一个或两个操作数,通常使用以下模板:

1
MNEMONIC{S}{condition} {Rd}, Operand1, Operand2

由于 ARM 指令集的灵活性,并非所有指令都使用模板中提供的所有字段

1
2
3
4
5
6
MNEMONIC - 指令的简称(助记符)
{S} - 可选后缀,如果指定了S,则条件标志会根据操作结果更新
{condition} - 执行指令需要满足的条件
{Rd} - 用于存储指令结果的寄存器(目标)
Operand1 - 第一个操作数,寄存器或立即数
Operand2 - 第二个(灵活的)操作数,可以是立即数(数字)或带有可选移位的寄存器

虽然 MNEMONIC、S、Rd 和 Operand1 字段是直截了当的,但条件和 Operand2 字段需要进一步澄清:

  • 条件字段与 CPSR 寄存器的值紧密相关,或者更确切地说,与寄存器中特定位的值紧密相关
  • Operand2 被称为灵活的操作数,因为我们可以以各种形式使用它:
    • 作为即时值(具有有限的值集)
    • 寄存器
    • 以移位寄存器
  • 例如,我们可以将这些表达式用作操作数 2:
1
2
3
4
5
6
7
#123 - 立即值(具有有限的一组值)
Rx - 寄存器 x(如 R1、R2、R3 ...)
Rx, ASR n - 寄存器 x 算术右移 n 位 (1 = n = 32)
Rx, LSL n - 寄存器 x 逻辑左移 n 位 (0 = n = 31)
Rx, LSR n - 寄存器 x 逻辑右移 n 位 (1 = n = 32)
Rx, ROR n - 寄存器 x 右移 n 位 (1 = n = 31)
Rx, RRX - 寄存器 x 右移一位,扩展
  • 作为不同类型说明的快速示例,让我们看一下以下列表:
1
2
3
4
ADD R0, R1, R2 - 将 R1 (Operand1) 和 R2 (Operand2 的寄存器形式) 的内容相加并将结果存储到 R0 (Rd)
ADD R0, R1, #2 - 将 R1 (Operand1) 的内容与值 2 (Operand2 的立即数形式) 相加并将结果存储到 R0 (Rd)
MOVLE R0, #5 - 仅当满足条件 LE (小于或等于)时,将数字 5 (操作数 2,因为编译器将其视为 MOVLE R0, R0, #5) 移动到 R0(Rd)
MOV R0, R1, LSL #1 - 将 R1 的内容(操作数 2 采用逻辑左移的寄存器形式)左移一位到 R0(Rd),因此,如果 R1 的值为 2,它会左移一位并变为 4,然后将 4 移至 R0

作为快速摘要,让我们看一下我们将在以后的示例中使用的最常见的说明

指令 描述 指令 描述
MOV Move data EOR Bitwise XOR
MVN Move and negate LDR Load
ADD Addition STR Store
SUB Subtraction LDM Load Multiple
MUL Multiplication STM Store Multiple
LSL Logical Shift Left PUSH Push on Stack
LSR Logical Shift Right POP Pop off Stack
ASR Arithmetic Shift Right B Branch
ROR Rotate Right BL Branch with Link
CMP Compare BX Branch and eXchange
AND Bitwise AND BLX Branch with Link and eXchange
ORR Bitwise OR SWI/SVC System Call

加载和存储

ARM 使用 load-store 模型进行内存访问,这意味着只有 load/store(LDR 和 STR)指令才能访问内存,虽然在x86上允许大多数指令直接对内存中的数据进行操作,但在ARM上,数据必须在操作之前从内存移动到寄存器中

这意味着在 ARM 上的特定内存地址递增 32 位值需要三种类型的指令(加载、递增和存储),首先将特定地址的值加载到寄存器中,在寄存器中递增,然后将其从寄存器存储回存储器

通常,LDR 用于将某些内容从内存加载到寄存器中,而 STR 用于将某些内容从寄存器存储到内存地址

常规操作

1
2
LDR R2, [R0] 	@ [R0]:原始地址是在 R0 中找到的值
STR R2, [R1] @ [R1]:目标地址是在 R1 中找到的值
  • LDR 操作:将 R0 中找到的地址处的值加载到目标寄存器 R2
  • STR 操作:将 R2 中找到的值存储到 R1 中找到的内存地址
1
2
STR    Ra, [Rb, imm]
LDR Ra, [Rc, imm]
  • 偏移形式为:使用立即(整数)作为偏移量
  • 偏移地址模式:先从基本寄存器 Ra 中添加或减去此值,然后再使用已知的偏移量访问数据
1
2
STR    Ra, [Rb, Rc]
LDR Ra, [Rb, Rc]
  • 偏移形式为:使用寄存器作为偏移(偏移地址模式)
    • Rb 是基寄存器
    • Rc 是偏移
  • 使用场景:当您的代码想要访问在运行时计算索引的数组
1
2
LDR    Ra, [Rb, Rc, <shifter>]
STR Ra, [Rb, Rc, <shifter>]
  • 偏移形式为:具有缩放寄存器作为偏移(偏移地址模式)
    • Rb 是基寄存器
    • Rc 是左/右移位(< shifter >)的即时偏移(或包含即时值的寄存器),以缩放即时偏移(这意味着桶形移位器用于缩放偏移)
  • 使用场景:循环以循环访问数组

特殊操作

1
LDR r3, [r1], #offset
  • 索引后地址模式:这意味着基寄存器(R1)用作最终地址,然后使用 R1 + 4 计算的失调进行更新
  • 换句话说,它采用在 R1(不是 R1+4)中找到的值并将其加载到 R3 中,然后将 R1 更新为 R1 + offset
1
ADR r0, words+12             /* address of words[3] -> r0 */
  • 我们使用 ADR 指令(懒惰方法)来获取 word[3] 的地址放入 R0
  • PS:其实这两个都是伪指令
    • ADR 是小范围的地址读取伪指令
    • LDR 是大范围的读取地址伪指令
1
2
LDR r1, array_buff_bridge    /* address of array_buff[0] -> r1 */
LDR r2, array_buff_bridge+4 /* address of array_buff[2] -> r2 */
  • 执行上述两条指令后,R1 和 R2 包含 array_buff[0]array_buff[2] 的地址
  • PS:对于数组,可以采用这种这种写法

同时处理多个值

有时,一次加载(或存储)多个值会更有效,为此,我们使用 LDM(加载多个)和 STM(存储多个)指令:

1
LDM r0, {r4,r5}              /* words[3] -> r4 = 0x03; words[4] -> r5 = 0x04 */
  • R0 中装有 word[3]
  • 我们使用一个命令加载了多个(2个数据块),该命令将:
    • R4 = 0x00000003
    • R5 = 0x00000004
  • PS:注意在合适的位置打上花括号
1
stm r1, {r4,r5}              /* r4 -> array_buff[0] = 0x03; r5 -> array_buff[1] = 0x04 */
  • R1 中装有 array_buff[0]
  • 我们使用一个命令存储了多个(2个数据块),该命令将:
    • array_buff[0] = 0x00000003
    • array_buff[1] = 0x00000004

条件执行

CPSR 中的 N、Z、C 和 V 位与 x86 上 EFLAG 寄存器中的 SF、ZF、CF 和 OF 位相同,它们将用于支持程序集级别的条件和循环中的条件执行

下表列出了可用的条件代码、其含义以及测试的标志的状态:

Condition Code Meaning (for cmp or subs) Status of Flags
EQ 相等 Z==1
NE 不相等 Z==0
GT 大于(有符号) (Z==0) && (N==V)
LT 小于(有符号) N!=V
GE 大于等于(有符号) N==V
LE 小于等于(有符号) (Z==1) \ \ (N!=V)
CS or HS 大于等于(无符号) C==1
CC or LO 小于(无符号) C==0
HI 大于(无符号) (C==1) && (Z==0)
LS 小于等于(无符号) (C==0) \ \ (Z==0)
MI 负数 N==1
PL 正数或零 N==0
AL True
NV False
VS 有符号溢出 V==1
VC 无符号溢出 V==0

Thumb 条件执行

ARM 存在多种允许条件执行的 Thumb 版本,某些 ARM 处理器版本支持“IT”指令,该指令允许在 Thumb 状态下有条件地执行最多 4 条指令

1
IT{x{y{z}}} cond
  • cond 指定 IT 块中第条指令的条件
  • x 指定 IT 块中第条指令的条件开关
  • y 指定 IT 块中第条指令的条件开关
  • z 指定 IT 块中第条指令的条件开关

IT 指令的结构是 “IF-Then-(Else)”,语法是两个字母 T 和 E 的构造:

  • IT 指的是 If-Then(下一个指令是有条件的)
  • ITT 指的是 If-Then-Then(接下来的2条指令是有条件的)
  • ITE 指的是 If-Then-Else(接下来的2条指令是有条件的)
  • ITTE 指的是 If-Then-Then-Else(接下来的3个指令是有条件的)
  • ITTEE 指的是 If-Then-Then-Else-Else(接下来的4个指令是有条件的)

IT 块内的每条指令都必须指定一个相同或逻辑相反的条件后缀:

  • 如果使用 ITE,则第一条和第二条指令 (If-Then) 必须具有相同的条件后缀,第三条指令 (Else) 必须具有前两条指令的逻辑反比
  • 以下是 ARM 参考手册中的一些示例,其中说明了此逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ITTE   NE           ; Next 3 instructions are conditional
ANDNE R0, R0, R1 ; ANDNE does not update condition flags
ADDSNE R2, R2, #1 ; ADDSNE updates condition flags
MOVEQ R2, R3 ; Conditional move

ITE GT ; Next 2 instructions are conditional
ADDGT R1, R0, #55 ; Conditional addition in case the GT is true
ADDLE R1, R0, #48 ; Conditional addition in case the GT is not true

ITTEE EQ ; Next 4 instructions are conditional
MOVEQ R0, R1 ; Conditional MOV
ADDEQ R2, R2, #10 ; Conditional ADD
ANDNE R3, R3, #1 ; Conditional AND
BNE.W dloop ; Branch instruction can only be used in the last instruction of an IT block

跳转&分支

分支(又名Jumps)允许我们跳转到另一个代码段,当我们需要跳过(或重复)代码块或跳转到特定函数时,这很有用

  • 这种用例的最佳示例是 IF 和 Loop

有三种类型的分支指令:

  • 分支(B)
    • 简单跳转到函数
  • 分支链接(BL)
    • 在 LR 中保存 (PC+4) 并跳转到功能
  • 分支交换(BX)和分支链路交换(BLX)
    • 与 B/BL 交换指令集相同(ARM <-> Thumb)
    • 需要寄存器作为第一个操作数:BX/BLX [reg]
  • BX/BLX 用于将指令集从 ARM 交换到 Thumb,示例如下:
1
2
3
4
5
6
7
8
9
10
.text
.global _start

_start:
.code 32 @ ARM mode
add r2, pc, #1 @ put PC+1 into R2
bx r2 @ branch + exchange to R2

.code 16 @ Thumb mode
mov r0, #1
  • 这里的诀窍是获取实际 PC 的当前值,将其增加“1”,将结果存储到寄存器,然后执行 BX/BLX [reg]
  • add r2, pc, #1:简单地获取有效的PC地址(即当前PC寄存器的值+8 -> 0x805C)并为其添加“1”(0x805C + 1 = 0x805D)

栈和函数

一般来说,堆栈是程序/进程中的内存区域,这部分内存是在创建进程时分配的

  • 我们使用 Stack 来存储临时数据,例如某些函数的局部变量,帮助我们在函数之间转换的环境变量等
  • 我们使用 PUSH 和 POP 指令与堆栈进行交互,如 PUSH 和 POP(这是其他一些内存相关指令的别名,而不是实际指令,但出于简单起见,我们使用 PUSH 和 POP)

首先,当我们说 Stack 增长时,我们的意思是一个项目(32位数据)被放在 Stack 上,堆栈可以 向上 增长(当堆栈以降序方式实现时)或 向下 增长(当堆栈以上升方式实现时),下一条(32位)信息的实际位置由堆栈指针定义,或者确切地说,由存储在SP寄存器中的内存地址定义

栈地址的增长方向:

  • 向高地址增长的栈称为 递增栈(Descendent Stack)
  • 向低地址增长的栈称为 递减栈(Acendant Stack)

作为不同 Stack 实现的摘要,我们可以使用下表,其中描述了在不同情况下使用哪些存储多个/加载多个指令:

堆栈类型 Store Load
完全降序 STMFD (STMDB,Decrement Before) LDMFD(LDM,Increment after)
完全升序 STMFA (STMIB,Increment Before) LDMFA (LDMDA,Decrement After)
空降序 STMED (STMDA,Decrement After) LDMED (LDMIB,Increment Before)
空升序 STMEA(STM,Increment after) LDMEA (LDMDB,Decrement Before)

函数

要理解 ARM 中的函数,我们首先需要熟悉函数的结构部分,它们是:

  1. Prologue(序幕)
  2. Body(主体)
  3. Epilogue(结语)

Prologue(序幕)的目的是保存程序的先前状态(通过将 LR 和 R11 的值存储到堆栈上),并为函数的局部变量设置堆栈(虽然序言的实现可能因使用的编译器而异,但通常这是通过使用 PUSH / ADD / SUB 指令来完成的)

1
2
3
push {r11, lr} 		/* 序幕的开始,将帧指针和LR保存到堆栈 */
add r11, sp, #0 /* 设置栈底帧 */
sub sp, sp, #16 /* 序幕结束,在堆栈上分配一些缓冲区(这也为堆栈帧分配空间) */

Body(主体) 部分通常负责某种独特而特定的任务,这部分函数可能包含各种指令,分支(跳转)到其他函数等,函数的 body 部分的示例可以像以下几条指令一样简单:

1
2
3
mov r0, #1 			/* 设置局部变量 (a=1)。 这也用作设置函数 max */ 的第一个参数
mov r1, #2 /* 设置局部变量 (b=2)。 这也用作设置函数 max */ 的第二个参数
bl max /* 调用/分支到函数 max */
  • 上面的示例代码显示了一个函数的片段,该函数设置局部变量,然后分支到另一个函数
  • 这段代码还向我们展示了函数的参数(在本例中为函数 max)是通过寄存器传递的
  • 在某些情况下,当有超过4个参数要传递时,我们会另外使用 Stack 来存储剩余的参数
  • 还值得一提的是,函数的结果通过寄存器 R0 返回,因此,无论函数(max)的结果是什么,我们应该能够在从函数返回后立即从寄存器 R0 中获取它
  • 需要指出的另一件事是,在某些情况下,结果的长度可能是64位(超过32位寄存器的大小)

函数的最后一部分,即 Epilogue(结语),用于将程序的状态恢复到其初始状态(在函数调用之前),以便它可以从它离开的位置继续,为此,我们需要重新调整堆栈指针(这是通过使用帧指针寄存器 R11 作为参考并执行添加或子操作来完成的)

重新调整堆栈指针后,通过将以前(在序幕中)保存的寄存器值从堆栈中弹出到相应的寄存器中来恢复它们,根据函数类型,POP 指令可能是 Epilogue 的最终指令(但是,可能是在恢复寄存器值后,我们使用 BX 指令离开函数),Epilogue 的示例如下所示:

1
2
sub sp, r11, #0 		/* 结尾的开始,重新调整堆栈指针 */
pop {r11, pc} /* 结尾,从堆栈中恢复帧指针,通过直接加载到PC中跳转到以前保存的LR,函数的堆栈帧最终在这一步被销毁 */

所以现在我们知道:

  1. Prologue(序幕)为功能设置环境
  2. Body(主体)实现函数的逻辑并将结果存储到 R0
  3. Epilogue(结语)还原状态,以便程序可以从调用函数之前离开的位置恢复

参考:编写 ARM 程序集|阿泽里亚实验室